package com.lakeworks.framework.security.filter;

import com.lakeworks.domain.SysChannelAuth;
import com.lakeworks.service.ISysChannelAuthService;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.lakeworks.common.constant.Constants.CHANNEL;
import static com.lakeworks.common.enums.ChannelControllerEnum.getNames;
import static com.lakeworks.common.utils.SecurityUtils.getAuthentication;
import static com.lakeworks.common.utils.spring.SpringUtils.getBean;
import static java.lang.Long.parseLong;
import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.Duration.between;
import static java.time.Instant.now;
import static java.time.Instant.ofEpochSecond;
import static java.util.Arrays.asList;
import static java.util.Objects.nonNull;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.compile;
import static org.apache.commons.codec.binary.Base64.encodeBase64String;
import static org.apache.commons.lang3.StringUtils.equalsAny;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.http.HttpHeaders.AUTHORIZATION;

/**
 * @Author: Auxps
 * @Date: 2023/3/2
 * Description:
 * 三方渠道认证
 */
@Component
public class ChannelAuthenticationFilter extends OncePerRequestFilter implements ApplicationRunner {

    @Value("${server.servlet.context-path}")
    private String contextPath;

    private static final String CHANNEL_PATTERN = "/channel/**";

    private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

    private static final HmacAlgorithms ALGORITHMS = HmacAlgorithms.HMAC_SHA_256;

    private static final Pattern AUTHORIZATION_PATTERN = compile("^ROC-(?<algorithm>\\w+)-(?<version>[\\d\\.]+)\\s+(?<ak>[\\w]+),(?<timestamp>\\d{10}),(?<nonce>[^,]+),(?<signature>[^,]+)$", CASE_INSENSITIVE);

    private static LinkedMultiValueMap<String, String> MULTI_VALUE_MAP;

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISysChannelAuthService sysChannelAuthService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (nonNull(getAuthentication()) || !ANT_PATH_MATCHER.match(contextPath.concat(CHANNEL_PATTERN), request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }
        /*Enumeration<String> headerNames = request.getHeaderNames();
        // 打印请求头信息
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            logger.info("request header--"+ headerName + ": " +headerValue);
        }*/
        ifUrlList(request);
        String authorization = request.getHeader(AUTHORIZATION);
        if (isBlank(authorization))
            throw new RuntimeException(format("Header [%s] empty", AUTHORIZATION));
        logger.info("Channel Authorization: [{}]", authorization);

        Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization);
        if (!matcher.matches())
            throw new RuntimeException(format("Header [%s] invalid format", AUTHORIZATION));

        if (!ALGORITHMS.getName().equalsIgnoreCase(matcher.group("algorithm")))
            throw new RuntimeException(format("Algorithm [%s] NOT supported", matcher.group("algorithm")));

        switch (matcher.group("version")) {
            case "1.0":
                signatureVerify(request, matcher);
                break;
            default:
                throw new RuntimeException(format("Auth spec version [%s] NOT supported", matcher.group("version")));
        }
        filterChain.doFilter(request, response);
    }

    private void signatureVerify(HttpServletRequest request, Matcher matcher) {
        //时间戳验证
        Instant instant = ofEpochSecond(parseLong(matcher.group("timestamp")));
        if (between(instant,now()).abs().toMinutes() >= 5)             //超过5分钟
            throw new RuntimeException("签名超时！");

        SysChannelAuth sysChannelAuth = sysChannelAuthService.selectByAk(matcher.group("ak").trim());

        if (sysChannelAuth == null)
            throw new RuntimeException("渠道不存在，ak：" + matcher.group("ak"));

        StringJoiner joiner = new StringJoiner("\n", "", "\n")
                .add(request.getMethod().toUpperCase())              //请求方式
                .add(request.getRequestURI().toLowerCase())          //uri
                .add(valueOf(matcher.group("timestamp")))      //时间戳
                .add(matcher.group("nonce"));                  //随机串

        byte[] hmacBytes = new HmacUtils(ALGORITHMS, sysChannelAuth.getSk().getBytes())
                .hmac(joiner.toString().getBytes(UTF_8));

        if (!encodeBase64String(hmacBytes).equals(matcher.group("signature")))
            throw new RuntimeException("认证失败");

        PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken(sysChannelAuth, null, asList(new SimpleGrantedAuthority(CHANNEL)));
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        RequestMappingHandlerMapping handlerMapping = getBean(RequestMappingHandlerMapping.class);
        // 获取url与类和方法的对应信息
        Map<RequestMappingInfo, HandlerMethod> map = handlerMapping.getHandlerMethods();
        LinkedMultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();

        map.entrySet().forEach(entry -> {
            if (equalsAny(entry.getValue().getMethod().getDeclaringClass().getName(), getNames())) {
                String type = "", url = "";
                RequestMappingInfo info = entry.getKey();
                for (RequestMethod requestMethod : info.getMethodsCondition().getMethods())
                    type = requestMethod.toString();
                for (String str : info.getPatternsCondition().getPatterns())
                    url = str;
                multiValueMap.add(type, url);
            }
        });
        this.MULTI_VALUE_MAP = multiValueMap;
    }

    private void ifUrlList(HttpServletRequest request) {
        List<String> urlList = MULTI_VALUE_MAP.get(request.getMethod());
        if (urlList == null)
            throw new RuntimeException("此url拒绝访问");
        boolean flag = true;
        for (String url : urlList) {
            if (ANT_PATH_MATCHER.match(contextPath.concat(url), request.getRequestURI())) {
                flag = false;
                break;
            }
        }
        if (flag) throw new RuntimeException("此url拒绝访问");
    }
}
